← ALL ARTICLES
渲染架构15 MIN READ

Deferred Rendering

全库目录221
本文目录19

几十盏灯照向同一个物体时,每盏灯都需要表面位置、法线和材质参数。Deferred Rendering把这份共同输入先保存到GBuffer,再让灯光读取它。几何与材质属性生成一次,影响表面的灯仍逐盏求值;能省下多少工作,要与写入和读取这些附件的成本一起计算。

一帧里还有透明表面、几何边缘和不同的材质模型。它们暴露的是同一个问题:GBuffer究竟保存了哪些表面,后续通道怎样解释这些数据。我们沿几何写入、空间重建、光源消费和颜色合成追踪一条完整路径,再看多样本与移动端怎样改变资源组织。

GBuffer与表面重建

几何写入与附件布局

不透明物体经过深度测试,只把当前最近表面的属性留在附件中。一次像素着色写多个颜色附件称为多渲染目标(Multiple Render Targets,MRT);GBuffer利用它把同一表面的多种属性同时保存下来。被前方物体挡住的表面没有进入这份结果,后续光照不能从中取回它。

下面给出一套贯穿全文的明确布局。基础色在线性空间,法线存观察空间单位方向;金属度与粗糙度都在[0,1],自发光使用线性HDR颜色。四个颜色附件合计24字节/像素,深度另计4字节。项目压缩布局时仍须保持相同解码语义。

附件格式每样本字节数数据与清理值
BaseMetalRGBA8_UNORM4RGB为线性基础色,A为金属度;清零
NormalRoughnessRGBA16_FLOAT8RGB为N*0.5+0.5,A为粗糙度;清零
EmissiveRGBA16_FLOAT8RGB为自发光,A保留;清零
ModelR32_UINT40为GGX材质,1为无光照材质;清为0xFFFFFFFF表示背景
DepthD32_FLOAT4设备深度;本文普通Z清为1,比较使用LESS

颜色附件的逐样本合计为4+8+8+4=24字节,加深度为28字节。这是本文示例的字段预算;接入Direct3D时分别对应R8G8B8A8_UNORM、R16G16B16A16_FLOAT、R32_UINT和D32_FLOAT。资源创建前查询目标设备对格式用途与样本数的支持,确认颜色写入、整数读取和多样本路径均可用。

背景标记按整数值精确初始化。D3D11的ClearRenderTargetView接收浮点参数,指定整数位模式时采用覆盖目标的全屏绘制,像素着色器向Model输出uint(0xFFFFFFFF)。使用完整目标视口,关闭深度/模板测试、混合、裁剪矩形和Alpha-to-Coverage,开启Model通道写入,并用全开样本掩码覆盖全部样本。其他颜色附件和深度按表清理,再进行几何写入。相应API若提供保证该整数值的清理接口,可使用其整数路径。

纹理的基础色采样若使用sRGB视图,采样器已把它解为线性值,再写入这里的UNORM附件;光照阶段按线性值读取。法线、粗糙度与金属度属于数值数据,采用线性视图。若把法线也做sRGB解码,0.5会变成另一个数,原本的零方向分量随之改变。格式和通道压缩的取舍见GBuffer布局。

以下HLSL机制片段接收已完成材质采样的值,输出与表中四个颜色附件对应。法线从对象空间进入观察空间之前,用对象到观察矩阵的逆转置变换,再归一化;法线贴图则先经正确的切线基底恢复。材质模型ID对一个图元保持离散,顶点接口使用nointerpolation uint。

hlsl
struct GBufferOutput {
    float4 baseMetal : SV_Target0;
    float4 normalRoughness : SV_Target1;
    float4 emissive : SV_Target2;
    uint model : SV_Target3;
};
GBufferOutput WriteGBuffer(float3 baseColor, float metallic,
                          float3 normalVS, float roughness,
                          float3 emissive, uint model) {
    GBufferOutput o;
    o.baseMetal = float4(baseColor, metallic);
    o.normalRoughness = float4(normalize(normalVS)*0.5+0.5, roughness);
    o.emissive = float4(emissive, 0);
    o.model = model;
    return o;
}

写入阶段启用深度写入,颜色混合关闭,四个目标与深度使用相同尺寸和样本数。Alpha裁剪材质先执行阈值丢弃,通过的片元才按不透明表面写入。平滑透明需要保留前后层,处理方式放在后面的合成阶段。

例如观察法线(0,0,1)、粗糙度0.4写为(0.5,0.5,1,0.4)。读取时前三分量乘2减1,再归一化,得到原方向。直接把存储的RGB当法线,会得到朝向(1,1,2)的方向,高光会偏移。背景的Model无效标记先被排除,背景法线清零不参与归一化。

深度重建与统一空间

表面位置可以从深度还原,省去一个位置附件。本文固定Direct3D式约定:纹理UV原点在左上,NDC的X向右、Y向上,设备深度在[0,1],矩阵按列向量计算。像素索引(x,y)对应UV为((x+0.5)/W,(y+0.5)/H)。逆投影矩阵必须是当前几何阶段投影矩阵的逆,包括本帧投影抖动。

hlsl
float3 ReconstructPositionVS(float2 uv, float deviceDepth)
{
    float4 positionH = mul(InvProjection,
        float4(uv.x*2.0-1.0, 1.0-uv.y*2.0, deviceDepth, 1.0));
    return positionH.xyz / positionH.w;
}

设备深度不是到相机的线性距离。逆矩阵把NDC还原为齐次观察坐标,最后除以w才恢复位置。忘记这次除法会让位置随深度产生错误缩放。Reversed-Z依然可以使用同样的逆投影过程,但清理、深度比较与投影矩阵必须成套切换;其他API的Y方向和深度区间也要在进入逆矩阵前按实际约定转换。推导见深度缓冲与位置重建。

还原出的PVS要与法线和光放在同一空间。本页把世界空间点光位置以Cvs=View*(Cws,1)转换,灯轴以Avs=View*(Aws,0)转换后归一化;观察矩阵是刚体变换,光半径保持世界单位。法线已经存为Nvs,表面朝光方向是normalize(Cvs-Pvs),朝相机方向是normalize(-Pvs)。方向光若存的是灯射出的方向,表面朝光向量还需取反。阴影投影需要世界位置时,再用逆观察矩阵把PVS变回世界空间,送入对应灯的阴影矩阵。

可以只旋转相机检验这份契约。原本相同的法线与来光方向(1,0,0),旋转后都成为(0,1,0),点积仍为1。若只变换法线,拿它与世界空间来光(1,0,0)点积,结果变为0,固定物体就会随相机转动而变暗。空间错误由此直接落到两份输入上。

光源消费与累加

材质解码与直接光照

光照先读取Model,排除背景;ID为1时直接输出基础色加自发光,ID为0时解码基础色、金属度、粗糙度和法线,再重建位置。一个像素只初始化一次自发光,随后逐灯累加直接光。若在每盏灯的绘制里都加入自发光,多加一盏灯就会让发光物体更亮。

GGX模型采用BRDF与微表面模型中的EvaluateDirectResponse:输入同一观察空间的N,V,L,F0=lerp(0.04,baseColor,metallic),粗糙度映射为alpha=max(roughness²,0.002)。它返回已经包含入射余弦的漫反射加镜面响应。调用者乘光源强度、距离/角度衰减和阴影可见性,保留其线性HDR结果,不能再乘一次NdotL。BRDF各项在主归属完整展开,延迟路径改变的是输入获取与调用位置。

对点光,令delta=Cvs-Pvs,d²=dot(delta,delta)。半径外贡献为零,内部采用下列有限范围衰减:

a(d)=[max⁡(0,1−(d/r)4)]2max⁡(d2,dmin2).a(d)=\frac{\left[\max(0,1-(d/r)^4)\right]^2}{\max(d^2,d_{min}^2)}.

半径r与最小距离dmin均以米计。样例取dmin=0.01米;有限范围窗口在边界降为零,灯中心的方向未定义,样例对d²<10^-12返回零,生产资产通过发光体尺寸与碰撞关系避免表面穿过理想点源。light.intensity是RGB光强;直接光输出为response*intensity*a(d)*visibility。聚光灯再乘由内外锥角定义的平滑角度权重,阴影、Cookie和IES的输入契约见直接光照。

这条计算链也说明“100盏灯只读一次材质”与“100盏灯只求值一次”是两件事。Compute可以在一次线程调用中载入表面后循环100次;逐灯Draw则可能反复读取同一GBuffer。两者都要累加100份影响,区别在覆盖、调度和访存复用。

全屏光照与光体积

方向光可以用全屏三角形覆盖,每个有效表面计算一次该灯。有限范围点光若也全屏计算,大量片元最后只得到零,因此可以先画球形覆盖网格,缩小进入片元阶段的区域,再做精确范围判断:

hlsl
float3 toLight = lightPositionVS - surfacePositionVS;
if (dot(toLight, toLight) >= lightRadius * lightRadius) discard;

覆盖网格只减少候选片元;场景点位于解析球内才接受。低面数球的三角面通常内接于球面,直接使用会漏掉边缘,所以网格须外接或保守放大。最容易核对的实现从全屏与解析判断开始,随后用相同求值替换为保守体积覆盖,逐像素比较两份贡献。

光照不写深度,逐灯Draw使用HDR目标的加法混合ONE,ONE。采用体积覆盖时,以闭合体积背面形成一次覆盖,几何光栅深度测试可先关闭,场景位置判断负责接收;相机进入体积、近面切开体积或保守性无法保证时切换全屏。场景位置测试直接决定该表面是否接收这盏灯。若目标引擎使用反向绕序或投影翻转,剔除面也随实际绕序配置。

深度模板剔除是进一步优化。完整的模板体积判定需要考虑前后面、场景深度和近面裁剪,单次深度失败只能说明这个体积片元在场景之后,不能证明场景点位于闭合体积内。共享模板时把分类与计数位域分开,避免计数覆盖对象标签。

聚光灯外接锥体

一份单位闭合锥体可以复用于不同灯。局部轴取+Z,锥尖位于原点,底面在z=1,N个底面顶点在单位圆上。本页距离截断半径为r,覆盖锥体取轴向长度L=r,外半角为θ,满足0<θ<π/2、N≥3。理想底面半径为L*tan(θ);正N边形边中点离中心只有顶点半径的cos(π/N)倍,因此把顶点半径再除以这个余弦,才能外接理想圆。

hlsl
float outerRadius = axialLength * tan(outerHalfAngle) / cos(PI / sideCount);
positionOS.xy *= outerRadius;
positionOS.z *= axialLength;

片段输入上述单位锥体,角度以弧度计;变形后的顶点经灯变换进入世界,再经相机矩阵进入裁剪空间。底盖与侧面一起闭合覆盖。片元同时判断|P-C|≤r和dot(normalize(P-C),axis)≥cos(θ),再计算内外锥之间的权重。若实际距离按球面半径截断,锥体只是保守候选,解析距离判断仍负责截去底角。

光体积片元的屏幕坐标用SV_Position.xy/renderSize得到。顶点阶段先除w再用默认透视插值传屏幕UV,会多引入一次透视校正,读取位置与实际片元不一致;用片元屏幕坐标或正确的齐次插值契约可以避免这类错位。

Tiled Deferred列表消费

小光体积很多时,逐灯Draw的提交与覆盖仍有成本。Tiled Deferred把屏幕分成16×16 Tile,提前给每块生成灯列表。光照Compute每像素读一次表面,再遍历对应列表;列表构建与光照之间有存储写入到Shader读取的可见性依赖,GBuffer颜色/深度写入到光照读取也有各自依赖。

列表以header=(offset,count)索引连续的灯索引数组。像素(35,20)落在Tile(2,1);若TilesX=8,头索引是1*8+2=10。头为(6,2)且索引数组第6、7项为[3,8],这一像素只取灯3和8。每盏灯仍执行解析距离和锥角判断,因为Tile相交只是保守筛选。Compute输出每像素的完整HDR和,不再用逐灯混合。

列表构建、深度区间与相交算法见Tiled与Clustered光源剔除。本页接入时要求offset+count落在有效数组内,灯索引小于总灯数;固定容量超限的Tile写溢出标记,消费端对该Tile回退到完整光源数组。静默截断会丢光,写穿数组会污染相邻Tile。透明层另有深度范围,不能直接复用仅由最近不透明深度裁出的列表。

透明与多样本合成

透明层的可见性与顺序

GBuffer最近深度只能保存一个表面。两层半透明玻璃都透出后方颜色,把最近一层材质写入GBuffer后,后面那层已被深度覆盖;把两个材质参数直接混合,又得不到两次反射与透射的结果。因此普通混合透明在不透明光照完成后另行着色,保留不透明深度用于遮挡。

具体顺序是:不透明几何写GBuffer与深度;光照得到线性HDR背景;透明物体从后向前绘制,深度测试开启、深度写入关闭;最后再做色调映射。透明材质在Forward阶段取得自己的表面法线与位置,使用同一光源单位和BRDF,计算颜色后合成。带透明深度范围的Forward+列表可以复用光源数据,只有不透明最近深度区间的列表不足以保证透明不漏灯。

采用预乘Alpha时,源颜色Cs=alpha*surfaceColor,合成Cout=Cs+(1-alpha)*Cdst,混合为ONE,ONE_MINUS_SRC_ALPHA。蓝色背景(0,0,1)上,远处绿色和近处红色的Alpha均为0.5:先远后近得到(0.5,0.25,0.25);顺序颠倒得到(0.25,0.5,0.25)。混合不交换,所以排序是算法的一部分。交叉透明网格无法仅靠对象距离排序解决,需要切分或采用透明合成中的多层表示与近似。

Alpha裁剪只决定覆盖,仍可以进入GBuffer;薄膜、折射等还需要后方颜色或深度时,显式读取已完成的不透明结果,避免一边写当前颜色、一边无同步地采样同一目标。多层折射的采样与排序需求继续由透明路径承担。

MSAA表面与颜色Resolve

MSAA让一个像素包含多个覆盖和深度样本。几何边缘的四个样本可能两个属于墙,两个属于背景;也可能分别属于材质完全不同的物体。这些样本保存的是多个表面,不能先平均它们的法线、粗糙度和ID,再把平均值当成一个真实表面求光照。

例如两个样本的法线为(0,0,1)和(0,0,-1),白色Lambert材质,来光沿+Z且单位照度。分别着色得到1/π和0,最终颜色平均为1/(2π)。先平均法线得到零向量,无法归一化,更无法代表原来的两块表面。两个不同材质ID平均也可能变成第三种材质,这与颜色Resolve的语义不同。

正确的基线路径把GBuffer、深度和HDR颜色都建立为同样的多样本数。光照按样本读取Texture2DMS.Load(pixel,sampleIndex),背景样本填背景色;有效样本用该样本的深度、材质和法线求值,再写对应HDR样本。位置重建的UV使用pixel+0.5+sampleOffset,偏移来自当前样本布局;只用像素中心会把不同样本错误地放在同一条视线上。像素着色器的SV_SampleIndex可请求逐样本执行,全屏覆盖所有样本,以单独的样本读写与几何结果对应。几何材质的连续输入采用sample插值,样例GBufferPS的法线和材质输入已如此声明,使逐样本位置与表面属性一致。

光照与透明都完成后,对线性HDR颜色做Resolve。例如四样本颜色[1,1,0,0]得到0.5,未覆盖的样本保留背景,不能按“有效样本数”除成1。色调映射是非线性运算,放在颜色平均之后;每样本先色调映射再平均会得到另一种结果。

完整逐样本着色成本高。优化可以先判断本像素所有样本的覆盖、表面身份和连续属性是否足够一致,一致时复用一个求值;几何边缘和材质分界保留逐样本路径。深度相同不能单独证明法线和材质相同,按阈值合并连续属性也是质量近似。分类结果、逐样本次数与差值图一起输出,才能知道节省了哪部分计算。

平台与材质扩展

移动端片上附件与依赖

分块式GPU可以在片上处理一个Tile的颜色与深度。如果几何结束时把GBuffer写到外部内存,光照又读回来,就增加一次往返;让相邻几何与光照阶段在兼容的渲染范围内共享附件,才有机会保留中间值。

采用Vulkan传统Render Pass的一条明确路线是:子通道0把GBuffer作为颜色附件写入,子通道1把同一附件作为input attachment读取,HDR作为颜色附件输出;深度同样作为input attachment读取,光照片元的固定深度测试关闭,按深度重建表面。子通道依赖覆盖COLOR_ATTACHMENT_OUTPUT/颜色写和深度写入所在的EARLY_FRAGMENT_TESTS|LATE_FRAGMENT_TESTS/深度模板写,到FRAGMENT_SHADER/INPUT_ATTACHMENT_READ,并使用BY_REGION表达帧缓冲局部依赖。附件布局随引用从颜色/深度写入布局转成对应只读布局。执行先后和内存可见性都满足以后,光照才可以消费几何结果。

input attachment只读取当前片元位置对应的输入,不是任意邻域纹理采样。GBuffer若仅在当前Render Pass内部使用,可配置瞬态用途、合适的延迟分配内存及结束时丢弃;HDR则存储供后处理使用。需要下一阶段采样深度、法线或其他GBuffer时,把相关附件存储并保持资源有效,不能仍按内部临时数据丢弃。

这份依赖创造片上复用条件,实际合并还受设备附件预算、样本数、格式和驱动实现影响。屏幕邻域效果或Compute Tiled光照不直接等价于这个input attachment子通道方案:选择Compute时,应安排可采样存储和相应资源屏障,并把外存成本计入比较。片上光照可以使用片元阶段读取的保守灯列表,列表若在Render Pass外先构建,就要保证覆盖当前所有待着色表面。

Khronos的subpasses样例给出了几何与光照共享附件的实现及设备条件。目标设备上用物理Tile数、附件存取和外存流量判断实际驻留;API里出现两个子通道,本身不能证明已合并。

自定义着色模型的写入与分派

材质着色被分成两段后,新模型的参数必须跨过GBuffer。仅替换几何阶段的材质Shader,会改变存下来的数;统一光照仍按原BRDF解释它们,因此不会自动获得新的反射规律。

本页的ID0使用GGX,ID1使用无光照:两者共享基础色与自发光,但ID1在光照循环前直接输出baseColor+emissive。这同时避免对它重复累加灯光。未知ID输出诊断洋红色,暴露生产与消费版本不一致;Model为背景标记时走背景路径。ID采用整数读取,逐样本保留,不能进行线性过滤。

例如卡通材质需要额外的明暗阈值与暗部色,可分配新ID2,并给它定义额外附件的布局或明确复用本模型闲置通道。写入端保存阈值和暗部色,解码端按ID2还原,逐灯端把NdotL映射为分段响应;阴影是否在分段前参与、多个灯怎样组合也属于该模型契约。参数若多到布局无法容纳,可以使用离散材质索引读取外部参数表,但表的版本和资源寿命必须覆盖当前光照。所有模型都使用同一套基础色字段,不能把另一模型仍在使用的粗糙度随意改成阈值。

新模型接入依次核对写入Pass、附件格式、解码、分类与求值分支,最后在不同灯数、MSAA边缘与透明材质上检查一致性。引擎接口可能通过扩展API或源码提供这些位置,版本相关入口见引擎渲染扩展导读。特殊材质选择独立Forward绘制时,还要从GBuffer光照中排除其重复贡献,并处理正确的深度和绘制顺序。

成本与实现检查

带宽与工作量

本文布局在单样本1920×1080下的逻辑容量为1920*1080*28=58,060,800字节,即58.06 MB或55.37 MiB;其中MB按10⁶字节、MiB按2²⁰字节计。预算只包含表中附件,HDR输出、交换链、额外Resolve资源、分配对齐和压缩分别计入。四样本的这部分逻辑容量为232,243,200字节。若每帧完整写一次并完整读一次,逻辑往返为116.12 MB,60帧/秒时约6.97 GB/s。这个模型固定了完整读写次数;背景跳过、逐灯重读、缓存、深度压缩与片上驻留会改变实际外存流量,测量结果由设备计数器给出。

Forward也可以一次绘制后循环多灯,因此比较不能把所有前向路径都假设为逐灯重复几何。Deferred主要把材质采样与灯数量分开,代价是附件写读、解码与后续合成。大量重叠灯且不透明占比高时,这份分离容易产生价值;灯少、带宽紧或透明占比高时,附件成本可能超过节省。

固定相机、分辨率、灯分布和材质,分别记录几何写入、列表构建、光照、透明及Resolve的GPU时间。逐灯关闭阴影再比较,可以把阴影成本与Deferred自身分开。平均灯数不能替代极端Tile的灯数,MSAA也记录边缘比例与逐样本着色次数。片上路线另外记录附件驻留和外存带宽,避免把逻辑字节数当成实测流量。

最小实现路径

Deferred.hlsl保留四目标GBufferPS、单样本LightingCS和逐样本LightingMSPS,光照入口均读取GBuffer并遍历Tile列表,其中展开GGX直接响应与点光衰减。文件按本文空间和格式约定编写,几何输入已完成材质采样,世界变换、列表构建和API资源创建由接入工程提供。

接入先建立单样本资源:清理附件,绘制不透明几何,生成保守灯列表,完成附件与列表的写后读同步,再以8×8线程组调度光照。TilesX=ceil(W/16),调度组数为ceil(W/8),ceil(H/8);边界线程提前退出。列表采用主归属的完整构建路径;最初也可把全部灯放入每个Tile,得到正确的对照结果,再检查剔除列表与完整列表的差值。样例Compute列表消费要求已完成溢出回退,数组与灯索引经过验证。

多样本路径将同格式、同样本数的GBuffer视图绑定到样例t8..t12,HDR颜色附件也使用相同样本数,列表覆盖所有有效样本的深度范围。以LightingMSPS画全屏三角形,深度测试关闭、混合关闭,每个样本独立写完整光照和;透明阶段完成后再对HDR做颜色Resolve。DepthMS.GetSamplePosition取得当前样本相对像素中心的偏移,所有附件保持相同样本布局。

将球体、硬边立方体与背景放在一张图里,分别显示Model、解码法线与重建位置。接着加入一点光和一盏锥角已知的聚光,比较全屏与光体积结果;然后加入两层颜色不同的透明平面,按前文数例核对顺序;最后打开MSAA,逐样本查看物体边缘和不同模型交界。每次改变一项,错误才容易对应到资源、空间或合成规则。

从仓库根目录运行:

powershell
python -B 知识库/examples/mechanism_chain/deferred/test_reference.py
dxc -T ps_6_0 -E GBufferPS 知识库/examples/mechanism_chain/deferred/Deferred.hlsl -Fo "$env:TEMP/kb-chain-deferred-gbuffer.dxil"
dxc -T cs_6_0 -E LightingCS 知识库/examples/mechanism_chain/deferred/Deferred.hlsl -Fo "$env:TEMP/kb-chain-deferred-lighting.dxil"
dxc -T ps_6_0 -E LightingMSPS 知识库/examples/mechanism_chain/deferred/Deferred.hlsl -Fo "$env:TEMP/kb-chain-deferred-msaa.dxil"

CPU参考的8项检查以手算输入覆盖Y轴、齐次除法、位置与方向变换、模型分派、透明顺序、无效平均法线和背景样本;它们支持这些数值关系。三个HLSL入口已实际通过DXC编译,完整引擎接入、绘制状态、逐样本画面与设备带宽仍需按上述场景观测。编译证明代码类型与入口合法,逐样本覆盖、附件绑定与混合关系仍由接入场景检查。

相关主题

参考资料